/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.console.config.bean;

import org.openislands.oi.console.config.ServiceManager;
import org.openislands.oi.console.utils.ClassScanUtils;
import org.openislands.oi.console.utils.ParameterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

public class BeanContainer {
    private static final Logger log = LoggerFactory.getLogger(BeanContainer.class);

    /***
     * Bean load handler. Load see static {}
     * See {@link #initBean(Set)}
     */
    private static final List<LoadBeanHandler> loadBeanHandlerList = new ArrayList<>();

    /***
     * Load bean container
     * See {@link LoadBeanHandler#loadBean(Set, Map)}
     */
    private static final Map<Class<?>, List<Object>> beanCache = new HashMap<>();

    public static <T> void initContainer(Class<T> startClass) {
        String[] packageArray;
        ComponentScan annotation = startClass.getAnnotation(ComponentScan.class);
        go:
        {
            if (annotation != null) {
                if (annotation.basePackages().length > 0) {
                    packageArray = annotation.basePackages();
                    break go;
                } else if (annotation.basePackageClasses().length > 0) {
                    Class<?>[] classes = annotation.basePackageClasses();
                    packageArray = Arrays.stream(classes).map(Class::getName).toArray(String[]::new);
                    break go;
                }
            }
            String className = startClass.getName();
            packageArray = new String[]{className.substring(0, className.lastIndexOf("."))};
        }

        Set<Class<?>> classes = ClassScanUtils.scan(packageArray);
        initContainer(classes);
    }

    public static void initContainer(Set<Class<?>> classes) {
        initBean(classes);
        initBeanField();
        refreshApplicationContext();
    }

    /***
     * See {@link LoadBeanHandler#loadBean(Set, Map)} {@link LoadBeanHandler#initBean(Map)}
     * @param classes scan class set
     */
    private static void initBean(Set<Class<?>> classes) {
        for (LoadBeanHandler loadBeanHandler : loadBeanHandlerList) {
            loadBeanHandler.loadBean(classes, beanCache);
        }

        for (LoadBeanHandler loadBeanHandler : loadBeanHandlerList) {
            loadBeanHandler.initBean(beanCache);
        }
    }

    /***
     * Fields in the initialization bean that may need to be processed
     */
    private static void initBeanField() {
        for (Map.Entry<Class<?>, List<Object>> entry : beanCache.entrySet()) {
            Class<?> entryKey = entry.getKey();
            for (Object obj : entry.getValue()) {
                loadResourceField(obj, entryKey.getFields());
                loadResourceField(obj, entryKey.getDeclaredFields());
            }
        }
    }

    /***
     * Set annotation present {@link Resource} or {@link Autowired} class field
     * Get value by {@link #beanCache}
     * @param self      class instance
     * @param fields    class fields
     */
    private static void loadResourceField(Object self, Field[] fields) {
        for (Field field : fields) {
            try {
                if (field.isAnnotationPresent(Resource.class) || field.isAnnotationPresent(Autowired.class)) {
                    Resource resource = field.getAnnotation(Resource.class);
                    Class<?> type = field.getType();
                    if (Objects.nonNull(resource) && resource.type() != Object.class) {
                        type = resource.type();
                    }

                    Object value = ParameterUtils.getFirst(beanCache.get(type));
                    if (value == null) {
                        for (Class<?> aClass : beanCache.keySet()) {
                            if (type.isAssignableFrom(aClass)) {
                                if ((value = ParameterUtils.getFirst(beanCache.get(aClass))) != null) break;
                            }
                        }
                    }
                    field.setAccessible(true);
                    field.set(self, value);
                    log.debug("init field: {}, value: {}", field.getName(), value);
                }
            } catch (IllegalAccessException e) {
                log.error(e.getMessage(), e);
            }
        }
    }

    @SuppressWarnings("all")
    private static void refreshApplicationContext() {
        for (Map.Entry<Class<?>, List<Object>> entry : beanCache.entrySet()) {
            for (Type anInterface : entry.getKey().getGenericInterfaces()) {
                if (anInterface instanceof ParameterizedType && ((ParameterizedType) anInterface).getRawType() == ApplicationListener.class
                        && ((ParameterizedType) anInterface).getActualTypeArguments()[0] == ContextRefreshedEvent.class) {
                    beanCache.get(entry.getKey()).forEach(obj -> ((ApplicationListener<ContextRefreshedEvent>) obj)
                            .onApplicationEvent(new ContextRefreshedEvent(
                                    new ClassPathXmlApplicationContext() {
                                        @Override
                                        public <T> Map<String, T> getBeansOfType(Class<T> type) throws BeansException {
                                            Map<String, T> ofType = new HashMap<>();
                                            beanCache.forEach((k, v) -> {
                                                if (type.isAssignableFrom(k))
                                                    for (int i = 0; i < v.size(); i++) {
                                                        ofType.put(k.getName() + "-" + i, (T) v.get(i));
                                                    }
                                            });
                                            return ofType;
                                        }
                                    }
                            ))
                    );
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBean(Class<T> clazz) {
        return (T) ParameterUtils.getFirst(beanCache.get(clazz));
    }

    static {
        loadBeanHandlerList.add(new LoadBean());
        loadBeanHandlerList.add(new CommandGroupContext());
        loadBeanHandlerList.add(new SingleCommandGroupContext());
        loadBeanHandlerList.add(new ParameterUtils());
        loadBeanHandlerList.add(new ServiceManager());
    }
}
